TD DSA 2021 de Antoine Ly - rapport de Fabien Faivre


4. Modélisation

4.1. Setup

import shap

shap.initjs()

4.1.1. Chargement des données

# On Importe les données

#df
df_train=pd.read_parquet('/mnt/data/interim/df_train.gzip')
df_val=pd.read_parquet('/mnt/data/interim/df_val.gzip')
df_test=pd.read_parquet('/mnt/data/interim/df_test.gzip')

#X
X_train=pd.read_parquet('/mnt/data/interim/X_train.gzip')
X_val=pd.read_parquet('/mnt/data/interim/X_val.gzip')
X_test=pd.read_parquet('/mnt/data/interim/X_test.gzip')

X_train_prepro=pd.read_parquet('/mnt/data/interim/X_train_prepro.gzip')
X_val_prepro=pd.read_parquet('/mnt/data/interim/X_val_prepro.gzip')
X_test_prepro=pd.read_parquet('/mnt/data/interim/X_test_prepro.gzip')

#y
y_train=pd.read_parquet('/mnt/data/interim/y_train.gzip')
y_val=pd.read_parquet('/mnt/data/interim/y_val.gzip')
y_test=pd.read_parquet('/mnt/data/interim/y_test.gzip')
pd.options.display.max_colwidth=300

4.2. Modélisation

4.2.1. Création du code générique

On commence par définir une fonction générique qui sera en capacité d’ajuster, optimiser et logger dans MLFlow les résultats de pipelines qui seront produits pour chaque essai

Le mode de fonctionnement souhaité consiste à

1- définir un pipeline au sens de sklearn

2- utiliser une fonction générique pour ajuster le pipeline (éventuellement en optimisant les paramètres) et en stocker le résultat dans MLFlow

4.2.1.1. Préalables : création des fonctions de résultat souhaitées

La première étape consiste à construire une fonction générique qui calculera les scores du pipeline que nous souhaitons suivre. Dans le cas présent comme l’exercice de classification est multiclasse, nous sommes intéressés par les f1, precision et recall calculés avec l’option macro qui réalise une moyenne des résultats obtenus par classe.

def score_estimator(
    estimator, X_train, X_test, df_train, df_test, target_col
):
    
    """
    Evalue un pipeline sur le jeu de train et test avec plusieurs métriques
    
    Ici les métriques utilisées sont :
    - f1 macro
    - precision macro
    - recall macro
    
    INPUTS :
        - estimator : un pipeline
        - X_train, X_test, df_train, df_test : les DataFrames contenant les jeux de données et test
        - target_col : le nom de la colonne cible dans les df
        
    OUTPUTS :
        - un DataFrame avec les métriques calculées sur les jeux de train et test fournis
    
    
    """

    metrics = [
        ("f1_macro", f1_score),   
        ("precision_macro", precision_score),
        ("recall_macro", recall_score),
        
    ]
    
    res = []
    for subset_label, X, df in [
        ("train", X_train, df_train),
        ("test", X_test, df_test),
    ]:
        y = df[target_col]
        y_pred = estimator.predict(X)
        for score_label, metric in metrics:
            score = metric(y, y_pred, average='macro')
            res.append(
                {"subset": subset_label, "metric": score_label, "score": score}
            )

    res = (
        pd.DataFrame(res)
        .set_index(["metric", "subset"])
        .score.unstack(-1)
        .round(4)
        .loc[:, ['train', 'test']]
    )
    return res

Pour pouvoir stocker les scores dans MLFlow, on les convertit en dictionnaires

def scores_to_dict(score_df):
    d = score_df['train'].to_dict()
    d1 = dict(zip([x+'_train_' for x in  list(d.keys())], list(d.values())))
    d = score_df['test'].to_dict()
    d2 = dict(zip([x+'_test' for x in  list(d.keys())], list(d.values())))
    d1.update(d2)
    return d1

Création d’une fonction affichant une matrice de confusion

def plot_cm(y_test, y_pred, target_names=[-1, 0, 1], 
            figsize=(5,3)):
    """Create a labelled confusion matrix plot."""
    cm = confusion_matrix(y_test, y_pred)
    fig, ax = plt.subplots(figsize=figsize)
    sns.heatmap(cm, annot=True, fmt='g', cmap='BuGn', cbar=False, 
                ax=ax)
    ax.set_title('Confusion matrix')
    ax.set_xlabel('Predicted')
    ax.set_xticklabels(target_names)
    ax.set_ylabel('Actual')
    ax.set_yticklabels(target_names, 
                       fontdict={'verticalalignment': 'center'});

4.2.1.2. Création de la fonction d’entraînement générique

La fonction suivante est celle qui sera systématiquement appélée pour entraîner les pipelines

Tip

L’évaluation fianle des modèles se faisant sur base de f1-macro dans le TD, c’est la métrique que nosu avons retenue pour la partie optimisation de la fonction générique

def trainPipelineMlFlow(mlf_XP, 
                        xp_name_iter, 
                        pipeline, 
                        X_train, y_train, X_test, y_test, 
                        target_col='sentiment', 
                        fixed_params={}, 
                        use_opti=False, iterable_params={}, n_iter=20):
    """
    Fonction générique permettant d'entrainer et d'optimiser un pipeline sklearn
    Les paramètres et résultats sont stockés dans MLFlow
    
    INPUTS:
        - mlf_XP : nom de l'experiment à créer dans MLFlow
        - xp_name_iter : nom du run créé dans l'experiment de MLFlow
        - pipeline : un pipeline au sens ed sklearn
        - X_train, y_train, X_test, y_test : des dataframes contenant les jeux d'entrainement et de test
        - target_col : le nom de la colonne du DataFrame y qui constitue la cible
        - fixed_params : un dictionnaire contenant les paramètres fixes dont l'utilisateur souhaite fixer la valeur dans le pipeline
        - use_opti : boolean, est-ce qu'une optimisation est recherchée. Si oui, utilisera RandomizedSearchCV
        - iterable_params : un dictionnaire contenant les nom des paramètres ciblés du pipeline et des listes contenant les valeusr possibles
        - n_iter : le nombre d'itérations maximales à réaliser par RandomizedSearchCV
    
    FONCTIONNEMENT:
        stocke dans MLFlow :
        - le pipeline entrainé
        - les principaux paramètres correspondant aux paramètres fixes et aux éventuels paramètres optimaux après RandomizedSearchCV
        - les scores (scalaires) calculés par la fonction score_estimator
        - le temps d'exécution
        
        imprime :
        - le nom de l'experiment
        - le pipeline entraîné
        - les paramètres principaux (cf FONCTIONNEMENT)
        - la matrice de confusion du pipeline sur le jeu de test fourni en entrée
    
    OUTPUTS:
        - le pipeline entraîné
    
       
    """
  
    mlflow.set_experiment(mlf_XP)

    with mlflow.start_run(run_name=xp_name_iter):
        
        start_time = time.monotonic()  
        
        warnings.filterwarnings("ignore")
        
        # fit pipeline
        pipeline.set_params(**fixed_params)
        if not use_opti:
            search = pipeline
        else:
            search = RandomizedSearchCV(estimator = pipeline, 
                                        param_distributions = iterable_params, 
                                        n_jobs = -1, 
                                        cv = 5, 
                                        scoring = 'f1_macro', 
                                        n_iter = n_iter,
                                        random_state = 42
                                       )
        
        search.fit(X_train, y_train[target_col])
                
        # get params
        params_to_log = fixed_params #select initial params
        if use_opti:
            params_to_log.update(search.best_params_) #update for optimal solution
        mlflow.log_params(params_to_log)
        
        # Evaluate metrics
        y_pred=search.predict(X_test)
        score = score_estimator(estimator=search, 
                                         X_train=X_train, 
                                         X_test=X_test, 
                                         df_train=y_train, 
                                         df_test=y_test, 
                                         target_col=target_col
                                )
        
        # Print out metrics
        print('XP :', xp_name_iter, '\n')
        print('pipeline : \n', search, '\n')
        print("params: \n", params_to_log, '\n')
        print('scores : \n', score, '\n')
        print("Test confusion matrix: \n")
        plot_cm(y_test, search.predict(X_test))
        
        
        #r Report to MlFlow
        mlflow.log_metrics(scores_to_dict(score))
        mlflow.sklearn.log_model(pipeline, xp_name_iter)
        
        end_time = time.monotonic()
        elapsed_time = timedelta(seconds=end_time - start_time)
        print('elapsed time :', elapsed_time)
        mlflow.set_tag(key="elapsed_time", value=elapsed_time)   
        
        
        
    return search;
        

4.2.1.3. Utilitaires : pour faciliter l’utilisation des pipelines

Si les pipelines permettent un traitement souple et homogène entre les jeux de données, leur manipulation n’est pas évidente. Notamment, le libellé des paramètres peut vide devenir délicat et difficilement lisible avec une combinaison de nom d’étape et du nom du paramètre dans l’étape du pipeline. La fonction suivante permet de rechercher tous les paramètres d’un pipeline qui contiennent une chaine de caractère spécifique.

def target_params(pipe, dict_keyval):
    """
    Crée un dictionnaire constitué de tous les paramètres incluant 'pattern' d'un pipe et leur assigne une valeur unique
    """
    
    res={}
    for key in list(dict_keyval.keys()):
    
        target = "[a-zA-Z\_]+__" + key

        rs = re.findall(target, ' '.join(list(pipe.get_params().keys())))
        rs=dict.fromkeys(rs, dict_keyval[key])
        res.update(rs)
    return res

4.2.1.4. Utilitaires : Adaptation des pipelines

La cellule suivante permet de créer des étapes de sélection de colonnes dans les Data Frame en entrée

from sklearn.base import BaseEstimator, TransformerMixin

class TextSelector(BaseEstimator, TransformerMixin):
    def __init__(self, field):
        self.field = field
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        if isinstance(X,(list,pd.core.series.Series,np.ndarray)): #permet d'avoir une structure souple si l'input n'est pas un DataFrame. Permet notamment d'utiliser LIME
            return X
        else:
            return X[self.field]

4.2.1.5. Utilitaires : Visualisation

def plotWc(text, stopwords=None, title=''):
    wc = WordCloud(
            stopwords=stopwords,
            width=800,
            height=400,
            max_words=1000,
            random_state=44,
            background_color="white",
            collocations=False
    ).generate(text)
    
    plt.figure(figsize = (10,10))
    plt.imshow(wc, interpolation="bilinear")
    plt.axis("off")
    plt.title(title)
    plt.show()

4.2.2. Approche initiale

On commence par construire un modèle simple qui nous servira de modèle de base que nous chercherons à améliorer.

Warning

Dans cette première étape, nous travaillerons sur le jeu train que nous avon découpé et évaluerons ses performances sur le jeu val.

Seuls les principaux modèles seront réentrainés sur { train + val } avant d’être évalués sur le véritable jeu test

On suit les modèles dans un DataFrame résultats

résultats = pd.DataFrame(columns=['modèle', 'f1_macro_val'])
résultats
modèle f1_macro_val

4.2.2.1. Bag of Words avec Random Forest

Dans cette expérimentation, nous créons un modèle simple :

BoW_RF

La classe TfidfVectorizer de sklearn permet d’appliquer ou non le traitement TF-IDF et dans ce dernier cas de travailler directement avec un Bag of Words

tfidf_RF_pipeline = Pipeline(
    steps=[
        ('coltext', TextSelector('text')),
        ("tfidf", TfidfVectorizer()),
        ("classifier", RandomForestClassifier(n_jobs=-1)),
    ]
)

Déjà dans cet exemple simple, le nombre de paramètres est important et leur nom vite complexe :

list(tfidf_RF_pipeline.get_params().keys())
['memory',
 'steps',
 'verbose',
 'coltext',
 'tfidf',
 'classifier',
 'coltext__field',
 'tfidf__analyzer',
 'tfidf__binary',
 'tfidf__decode_error',
 'tfidf__dtype',
 'tfidf__encoding',
 'tfidf__input',
 'tfidf__lowercase',
 'tfidf__max_df',
 'tfidf__max_features',
 'tfidf__min_df',
 'tfidf__ngram_range',
 'tfidf__norm',
 'tfidf__preprocessor',
 'tfidf__smooth_idf',
 'tfidf__stop_words',
 'tfidf__strip_accents',
 'tfidf__sublinear_tf',
 'tfidf__token_pattern',
 'tfidf__tokenizer',
 'tfidf__use_idf',
 'tfidf__vocabulary',
 'classifier__bootstrap',
 'classifier__ccp_alpha',
 'classifier__class_weight',
 'classifier__criterion',
 'classifier__max_depth',
 'classifier__max_features',
 'classifier__max_leaf_nodes',
 'classifier__max_samples',
 'classifier__min_impurity_decrease',
 'classifier__min_impurity_split',
 'classifier__min_samples_leaf',
 'classifier__min_samples_split',
 'classifier__min_weight_fraction_leaf',
 'classifier__n_estimators',
 'classifier__n_jobs',
 'classifier__oob_score',
 'classifier__random_state',
 'classifier__verbose',
 'classifier__warm_start']

En première intention on ajuste le pipeline sur le jeu d’entraînement avant les étapes de preprocessing réalisées lors de l’EDA

pipe = tfidf_RF_pipeline


base_tfidf_RF_= trainPipelineMlFlow(
                    mlf_XP = "Rapport",
                    xp_name_iter = "base_tfidf_RF", 
                    pipeline = pipe, 
                    X_train = X_train, y_train = y_train, X_test = X_val, y_test = y_val,
                    target_col = 'sentiment',
                    fixed_params = target_params( pipe , {'n_jobs':-1, 'random_state':42})
                    );
XP : base_tfidf_RF 

pipeline : 
 Pipeline(steps=[('coltext', TextSelector(field='text')),
                ('tfidf', TfidfVectorizer(use_idf=False)),
                ('classifier',
                 RandomForestClassifier(n_jobs=-1, random_state=42))]) 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42} 

scores : 
 subset            train    test
metric                         
f1_macro         0.9991  0.6781
precision_macro  0.9991  0.7071
recall_macro     0.9991  0.6676 

Test confusion matrix: 

elapsed time : 0:00:03.469991
../_images/test 4-mod_55_1.png

Le modèle de base produit un f1 macro de 67,81% sur le jeu de validation avec le paramétrage par défaut de sklearn. On observe le très fort f1 macro sur le jeu d’entraînement qui indique un fort surapprentissage. L’intérêt de ce pipeline est d’être très rapide à l’entraînement (à peine plus de 3 secondes ici)

On voit bien que la difficulté viendra de la classe neutre qui peut facilement être confondue avec les classes négatives ou positives

Par contre, il est plus surprenant de voir des tweets positifs classés en négatifs et inversement.

La suite investigue ce phénomène

pipe = base_TfIdf_RF_

y_val_pred = pipe.predict(X_val)

exemples_realNeg_predPos = X_val[(y_val['sentiment']==-1) & (y_val_pred==1)]
exemples_realPos_predNeg = X_val[(y_val['sentiment']==1) & (y_val_pred==-1)]
4.2.2.1.1. Analyse des tweets négatifs classé positifs
exemples_realNeg_predPos
text
22050 oo noo thats not good
22195 Legs are hurting because I was standing up all day.
22218 Is beastypops tired? I wish i was. My tablets are just making me want to throw up!
22278 BAH, you`be made me all hungry now
22349 just tried DMing you but it tried to download some strange file so stopped! How come no gmail MaccyM? Missing you SADS!!
... ...
27195 i wish i could but it would cost too much to call you all the way from the UK
27219 I can`t believe you tweeted that. It was our special moment
27329 unfortunetly no I wish, I mean sometimes like twice a yr they`ll have a party, but not always
27364 I bet you received lots of hit from that tweet; at work i cannot, wish i could
27475 wish we could come see u on Denver husband lost his job and can`t afford it

119 rows × 1 columns

plotWc(" ".join(exemples_realNeg_predPos['text']), stopwords=stopwords.words('english'), title = "Wordcloud des tweets négatifs prédits positifs")
../_images/test 4-mod_61_0.png

On voit les limites des approches de type Bag of Words : le modèle est trompé par des mots à connotation positive sans en comprendre l’enchaînement

4.2.2.1.2. Analyse des tweets positifs classé négatifs
exemples_realPos_predNeg
text
22078 dont worry im not!!! i dont get it on my tv
22165 Bummer I know LOL I actually do more partying when i am in school then out of school..I think it somehow helps me..hahaha!
22281 _22 ok so I`m having a complete insomniac moment. It`s 6am(almost) and I`m STILL awake. I hate when I can`t stop thinking! mornin!
22319 OMG I`M SOO EXCITED! i`ve been waiting for it ever since i saw the 5th one at midnight the night before!
22630 let me guess ... ran a few miles? Respect dude, I can`t do it. Maybe you should train me
22636 I`m gonna havta temp stop fllwing u while ur talkin abt kobe bc I loveeeeeeee him & I`m taking it personal and I like lebron 2.
23235 That poor girl on britains got talent, god love her forgot the words and cried but gets a second chance to perform again : ]
23259 Gmorning ooh giirll, Mondays
23499 Chilling feeling really nice..
23500 Hey, nothing wrong with that!
23536 I gave up cable in these tough economic times. it was either cable or shoes, and you know what cable lost
23610 _mcc bye, Scotty! i`m gonna miss you. ily<333
23695 _Honi Might be cute to do a little picture book called 'The little book of boring'
23746 aww i hope it does fly by because JT episodes are usually really good (and it`s early but so far this ep hassn`t disappointed)
23857 i want one so bad get one for me ?? (:
23860 I want some pineapple! I miss my baby
23864 on that note - i do not feel missed.
23883 congrats to the A`s!! ugh, we still have til the end of june
23886 let me know how it goes I`m praying. Ummmph. I still can`t believe it.
23915 is feeling good.. kinda tired.. miss him... can`t wait for grad this weekend!!
24016 morning world, is raining 2day so revision don`t seem so tough,
24191 Good luck with the footage - none of the stations are breaking in live with it
24413 Hello there! Thankyou. I always seem to make a difference in someone`s life everyday.
24840 The Beatles? Those scousers with funny haircuts? More talent in The Banana Splits!
24979 is so excited for this summer... Steve Winwood and Eric Clapton, Eagles of Death Metal, and ahhh the Dead Weather
25120 Yes, Cathy. (Ordinarily, I don`t have much of a problem with naked girls chasing me, however! )
25192 i do i do, i feel absoulutley fine
25367 I really like Lady GaGa`s 'Paparazzi'... #whatshappening
25389 fair enough. actually, you dont have to give me x-men. mad max will do me fine
25440 Romantic evening in with papa murphys and 'battle BC' from the history channel
25559 No nothing wrong with thinking he`s hot, but he belongs to me
25891 don`t be worried! I`m safe and sound! <3 you!
26025 What`s up yall! I made it an early night! I think ima bout to take a shower and chill witcha! Did I miss anything?
26149 baaha & healthy choice my friend! (:
26318 Hey! It`s so funny - I stopped to get a coffee on the way in and spilt half of it in the car!
26436 awww the wee gril in britains got talent
26494 Hahahaha! It`s not horrible, if others were singing with I`m sure it could work. I wish I could afford my own drum set
26519 Its raining cats and dogs here, in Mysore! Thankfully, no pigs/swines!
26574 Work, work, work. Finally not sick, though.
26665 I`m so bunged up!! I Hate colds!!
26673 I`ve burnt my collerbone and arms and face! aww!
26683 I wish I was going to Internet Week
26870 is problem free for now. atleast i already said to that person the truth.
26880 Feeling inspired this evening, huh?
27046 is having a well-deserved break today..NO PHONE CALLS, NO EMAILS..only plenty of catch up movies to doooooo
27067 No, i dont think its bad. And its very well edited, too.
27111 i want so bad to go to the mcfly`s concert
27190 friday night is my fav night of the week but now I have to go to stupid dog training classes
27212 Aww chamber callbacks... Soo emotional
27225 Gonna run to the gym to get my workout in before my really f`kin big pile of mulch arrives at around 8 or 9am! I`m excited about my mulch
27323 is liking this feeling
27427 i hate my presentation hahah whatever im glad its over
27473 So I get up early and I feel good about the day. I walk to work and I`m feeling alright. But guess what... I don`t work today.

On observe deux phénomènes contraires :

  • vraisemblablement des problèmes de labelisation (ex 22281 classé positifs…)

  • des tweets manifestements positifs sans pièges et pourtant classés négatifs (25367)

plotWc(" ".join(exemples_realPos_predNeg['text']), stopwords=stopwords.words('english'), title = "Wordcloud des tweets positifs prédits négatifs")
../_images/test 4-mod_66_0.png

pour essayer de comprendre ce qu’il s’est passé sur l’instance 22238, on peut essayer d’analyser le résultat à partir de Lime :

exemples_realPos_predNeg['text'][25367]
"I really like Lady GaGa`s 'Paparazzi'...  #whatshappening"
from lime import lime_text
from lime.lime_text import LimeTextExplainer
explainer = LimeTextExplainer(class_names=['negative', 'neutral', 'positive'])

exp = explainer.explain_instance(exemples_realPos_predNeg['text'][25367], base_TfIdf_RF_.predict_proba, top_labels=1)
exp.show_in_notebook(text=True)

Le résultat est très surprenant. On peut imaginer que really est utilisé plus frequemment dans des tweets négatifs

def list_words_with(text_series, search='', nb=30):
    '''
    Cette fonction permet de lister les mots dans un string qui contiennent une certaine chaîne de caractères
    
    inputs :
        - text_series : un pd.Series contennat les chaînes de caractères
        - search : la séquence à rechercher
        - nb : ressortir les nb occurences les plus fréquentes
    
    output :
        - une liste de tuples contenant 
            + le mot contenant la séquence recherchée
            + le nombre d'occurence dans text_series
    
    '''
    
    
    #searchPattern   = f"\w*{search}\w*"
    searchPattern   = f"\w*{search}\w* "
    
    cnt = Counter()
    
    for tweet in text_series:
        # Replace all URls with 'URL'
        tweet = re.findall(searchPattern,tweet)
        for word in tweet:
            cnt[word] += 1
    return cnt.most_common(nb)
    
list_words_with(X_train['text'][y_train['sentiment']==-1], 'really')
[('really ', 240), ('reallyreally ', 1), ('reallyyyyy ', 1), ('reallyy ', 1)]
list_words_with(X_train['text'][y_train['sentiment']==1], 'really')
[('really ', 202)]

Ce qui est bien le cas

modèle f1_macro_val
0 base_TfIdf_RF_ 0.669789

4.2.2.2. variante preprocessing

On peut juger de l’intérêt des prétraitements que nous avons réalisés en changeant le jeu d’entrée :

RF_prepro

pipe = tfidf_RF_pipeline


base_TfIdf_RF_prepro_= trainPipelineMlFlow(
                            mlf_XP = "Rapport",
                            xp_name_iter = "base_TfIdf_RF_prepro", 
                            pipeline = pipe, 
                            X_train = X_train_prepro, y_train = y_train, X_test = X_val_prepro, y_test = y_val,
                            target_col = 'sentiment',
                            fixed_params = target_params( pipe , {'n_jobs':-1, 'random_state':42})
                        );
XP : base_TfIdf_RF_prepro 

pipeline : 
 Pipeline(steps=[('coltext', TextSelector(field='text')),
                ('tfidf', TfidfVectorizer(use_idf=False)),
                ('classifier',
                 RandomForestClassifier(n_jobs=-1, random_state=42))]) 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42} 

scores : 
 subset            train    test
metric                         
f1_macro         0.9974  0.7079
precision_macro  0.9975  0.7208
recall_macro     0.9973  0.7028 

Test confusion matrix: 

elapsed time : 0:00:03.568283
../_images/test 4-mod_79_1.png

apport du preprocessing

On observe tout de suite l’apport des retraitemenst effectués à l’étape EDA : le modèle est passé à une performance de 70,79% sur le jeu de validation sans autres modifications

modèle f1_macro_val
0 base_TfIdf_RF_ 0.669789
0 base_TfIdf_RF_prepro_ 0.707919

4.2.2.3. variante optimisée

Une autre variante consiste à essayer d’ajuster les hyper paramètres du pipeline dans l’espoire de gagner en performance. On reste sur le jeu de données prétraité

pipe = tfidf_RF_pipeline


params = target_params( pipe , {
                                "use_idf": [True, False],
                                "ngram_range": [(1, 1), (1, 2), (1,3)],
                                "bootstrap": [True, False],
                                "class_weight": ["balanced", None],
                                "n_estimators": [100, 300, 500],
                                })


base_TfIdf_RF_prepro_opti_= trainPipelineMlFlow(
                                            mlf_XP = "Rapport",
                                            xp_name_iter = "base_TfIdf_RF_prepro_opti", 
                                            pipeline = pipe, 
                                            X_train = X_train_prepro, y_train = y_train, X_test = X_val_prepro, y_test = y_val,
                                            target_col = 'sentiment',
                                            fixed_params = target_params( pipe , {'n_jobs':-1, 'random_state':42}),
                                            use_opti = True,
                                            iterable_params = params,
                                            n_iter = 30
                                      );
XP : base_TfIdf_RF_prepro_opti 

pipeline : 
 RandomizedSearchCV(cv=5,
                   estimator=Pipeline(steps=[('coltext',
                                              TextSelector(field='text')),
                                             ('tfidf',
                                              TfidfVectorizer(use_idf=False)),
                                             ('classifier',
                                              RandomForestClassifier(n_jobs=-1,
                                                                     random_state=42))]),
                   n_iter=30, n_jobs=-1,
                   param_distributions={'classifier__bootstrap': [True, False],
                                        'classifier__class_weight': ['balanced',
                                                                     None],
                                        'classifier__n_estimators': [100, 300,
                                                                     500],
                                        'tfidf__ngram_range': [(1, 1), (1, 2),
                                                               (1, 3)],
                                        'tfidf__use_idf': [True, False]},
                   random_state=42, scoring='f1_macro') 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42, 'tfidf__use_idf': False, 'tfidf__ngram_range': (1, 1), 'classifier__n_estimators': 500, 'classifier__class_weight': 'balanced', 'classifier__bootstrap': False} 

scores : 
 subset            train    test
metric                         
f1_macro         0.9974  0.7064
precision_macro  0.9972  0.7110
recall_macro     0.9975  0.7041 

Test confusion matrix: 

elapsed time : 0:55:30.032064
../_images/test 4-mod_85_1.png

L’optimisation a eu ici un impact négatif très faible : f1 macro de 70,64% sur le jeu de validation, soit -0,15% , pour un temps de calcul démultiplié (55min vs 3sec)

modèle f1_macro_val
0 base_TfIdf_RF_ 0.669789
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432

4.2.2.4. Bag of Words avec régression logistique

Une autre variante : on essaie un autre classifier, la régression logistique

bow_pipeline_LR

tfidf_LR_pipeline = Pipeline(
    steps=[
        ('coltext', TextSelector('text')), 
        ("tfidf", TfidfVectorizer()),
        ("classifier", LogisticRegression(solver='liblinear', multi_class='auto')),
    ]
)
list(bow_pipeline_LR.get_params().keys())
['memory',
 'steps',
 'verbose',
 'coltext',
 'tfidf',
 'classifier',
 'coltext__field',
 'tfidf__analyzer',
 'tfidf__binary',
 'tfidf__decode_error',
 'tfidf__dtype',
 'tfidf__encoding',
 'tfidf__input',
 'tfidf__lowercase',
 'tfidf__max_df',
 'tfidf__max_features',
 'tfidf__min_df',
 'tfidf__ngram_range',
 'tfidf__norm',
 'tfidf__preprocessor',
 'tfidf__smooth_idf',
 'tfidf__stop_words',
 'tfidf__strip_accents',
 'tfidf__sublinear_tf',
 'tfidf__token_pattern',
 'tfidf__tokenizer',
 'tfidf__use_idf',
 'tfidf__vocabulary',
 'classifier__C',
 'classifier__class_weight',
 'classifier__dual',
 'classifier__fit_intercept',
 'classifier__intercept_scaling',
 'classifier__l1_ratio',
 'classifier__max_iter',
 'classifier__multi_class',
 'classifier__n_jobs',
 'classifier__penalty',
 'classifier__random_state',
 'classifier__solver',
 'classifier__tol',
 'classifier__verbose',
 'classifier__warm_start']
pipe = tfidf_LR_pipeline


params = target_params( pipe , {
                                "use_idf": [True, False],
                                "ngram_range": [(1, 1), (1, 2), (1,3)],
                                "class_weight": [None, 'balanced']
                                })    

TfIdf_LR_prepro_opti_ = trainPipelineMlFlow(
                                mlf_XP = "Rapport",
                                xp_name_iter = "TfIdf_LR_prepro_opti", 
                                pipeline = pipe, 
                                X_train = X_train_prepro, y_train = y_train, X_test = X_val_prepro, y_test = y_val,
                                target_col = 'sentiment',
                                fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42}),
                                use_opti = True,
                                iterable_params = params,
                                n_iter=30
                                );
XP : TfIdf_LR_prepro_opti 

pipeline : 
 RandomizedSearchCV(cv=5,
                   estimator=Pipeline(steps=[('coltext',
                                              TextSelector(field='text')),
                                             ('tfidf', TfidfVectorizer()),
                                             ('classifier',
                                              LogisticRegression(n_jobs=-1,
                                                                 random_state=42,
                                                                 solver='liblinear'))]),
                   n_iter=30, n_jobs=-1,
                   param_distributions={'classifier__class_weight': [None,
                                                                     'balanced'],
                                        'tfidf__ngram_range': [(1, 1), (1, 2),
                                                               (1, 3)],
                                        'tfidf__use_idf': [True, False]},
                   random_state=42, scoring='f1_macro') 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42, 'tfidf__use_idf': True, 'tfidf__ngram_range': (1, 1), 'classifier__class_weight': 'balanced'} 

scores : 
 subset            train    test
metric                         
f1_macro         0.8057  0.6986
precision_macro  0.8097  0.7041
recall_macro     0.8027  0.6947 

Test confusion matrix: 

elapsed time : 0:00:07.844044
../_images/test 4-mod_93_1.png

Le classifier LogisticRegression avec les données retraitées performe moins bien que le RandomForest (69,86% sur le jeu de validation).

modèle f1_macro_val
0 base_TfIdf_RF_ 0.669789
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432
0 TfIdf_LR_prepro_opti_ 0.698565

Afin de vérifier si les données retraitées apprortent quelque chose on relance le même pipeline avec les jeux d’origine

pipe = tfidf_LR_pipeline


params = target_params( pipe , {
                                "use_idf": [True, False],
                                "ngram_range": [(1, 1), (1, 2), (1,3)],
                                "class_weight": [None, 'balanced']
                                })    

TfIdf_LR_opti_ = trainPipelineMlFlow(
                        mlf_XP = "Rapport",
                        xp_name_iter = "TfIdf_LR_opti", 
                        pipeline = pipe, 
                        X_train = X_train, y_train = y_train, X_test = X_val, y_test = y_val,
                        target_col = 'sentiment',
                        fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42}),
                        use_opti = True,
                        iterable_params = params,
                        n_iter=30
                        );
XP : TfIdf_LR_opti 

pipeline : 
 RandomizedSearchCV(cv=5,
                   estimator=Pipeline(steps=[('coltext',
                                              TextSelector(field='text')),
                                             ('tfidf', TfidfVectorizer()),
                                             ('classifier',
                                              LogisticRegression(n_jobs=-1,
                                                                 random_state=42,
                                                                 solver='liblinear'))]),
                   n_iter=30, n_jobs=-1,
                   param_distributions={'classifier__class_weight': [None,
                                                                     'balanced'],
                                        'tfidf__ngram_range': [(1, 1), (1, 2),
                                                               (1, 3)],
                                        'tfidf__use_idf': [True, False]},
                   random_state=42, scoring='f1_macro') 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42, 'tfidf__use_idf': True, 'tfidf__ngram_range': (1, 1), 'classifier__class_weight': 'balanced'} 

scores : 
 subset            train    test
metric                         
f1_macro         0.8006  0.6999
precision_macro  0.8035  0.7031
recall_macro     0.7982  0.6973 

Test confusion matrix: 

elapsed time : 0:00:10.537456
../_images/test 4-mod_98_1.png

On observe un comportement différent avec la régression logistice. Dans ce cas, c’est l’utilisation du jeu d’origine (avec le tokeniser par défaut de Tf Idf) qui apport de meilleurs résultats (69,99% sur le jeu de validation), sans toutefois égaler les performances du RandomForest

modèle f1_macro_val
0 base_TfIdf_RF_ 0.669789
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432
0 TfIdf_LR_prepro_opti_ 0.698565
0 TfIdf_LR_opti_ 0.699877

Ainsi les méthodes classiques nous aurons permi de gagner 3 points de f1 macro, le leader actuel étant le modèle RandomForest avec un simple BagOfWords (TfIdf=False) et optimisé dans ses paramètres sur le jeu de données prétraité.

modèle f1_macro_val
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432
0 TfIdf_LR_opti_ 0.699877
0 TfIdf_LR_prepro_opti_ 0.698565
0 base_TfIdf_RF_ 0.669789

4.2.2.5. Optimisation du seuil de décision pour maximiser le f1

On peut aussi tirer avantage de la métrique utilisée pour l’évaluation. En effet, parmi les 3 catégories recherchées (negative, neutral et positive) il existe une gradation et en définitive, on est surtout intéressés à déterminer qsi un commentaire est positif ou négatif. La classification neutre étant une catégorie “par défaut” sans marqueur fort.

Stratégie : on maximise sur le jeu d’entraînement le seuil pour la décision positive, puis sur les non positifs, on maximise le seuil pour les négatifs, le reste est neutre

# permet de prendre une décision à partir d'un seuil
def to_labels(pos_probs, threshold):
    return (pos_probs >= threshold).astype('int')
def find_optimal_f1_thresholds(pipe, X, y):
    
    probs = pipe.predict_proba(X)
    
    # On commence par travailler les prédictions positives
    pos_probs = probs[:,2]
    # On définit une échelle de seuils
    thresholds = np.arange(0, 1, 0.001)
    # On évalue le f1 pour chaque seuil
    scores = [f1_score([(1 if i==1 else 0) for i in y ], to_labels(pos_probs, t)) for t in thresholds]
    # On récupère le seuil optimal pour la catégorie positive
    ix = np.argmax(scores)

    
    res = {'pos_threshold' : thresholds[ix], 'pos_f1' : scores[ix] }
    
    # On continue avec les prédictions négatives
    neg_probs = probs[:,0]
    # On définit une échelle de seuils
    thresholds = np.arange(0, 1, 0.001)
    # On évalue le f1 pour chaque seuil
    scores = [f1_score([(1 if i==-1 else 0) for i in y ], to_labels(neg_probs, t)) for t in thresholds]
    # On récupère le seuil optimal pour la catégorie positive
    ix = np.argmax(scores)

    
    res.update({'neg_threshold' : thresholds[ix], 'neg_f1' : scores[ix] })
    
    return res
    
# startégie : on commence par décider si positif,
# sur les non positifs

def sentiment_predict(pipe, X, dict_thres):
    '''
    stratégie :  on commence par décider si positif,
                 sur les non positifs, on décide si négatifs,
                 les restants sont neutres
    '''
    
    
    
    seuil_pos=dict_thres['pos_threshold']
    seuil_neg=dict_thres['neg_threshold']

    probs = pipe.predict_proba(X)

    y_test_pred_pos = to_labels(probs[:,2], seuil_pos)
    y_test_pred_neg = to_labels(probs[:,0], seuil_neg)

    y_test_pred = y_test_pred_pos
    y_test_pred[(y_test_pred_pos==0)] = -y_test_pred_neg[(y_test_pred_pos==0)]
    return y_test_pred
thres = find_optimal_f1_thresholds(base_TfIdf_RF_prepro_opti_, X_train_prepro, y_train['sentiment'])
thres
{'pos_threshold': 0.39,
 'pos_f1': 0.997985031663788,
 'neg_threshold': 0.417,
 'neg_f1': 0.9969359780680535}
y_val_pred = sentiment_predict(base_TfIdf_RF_prepro_opti_, X_val_prepro,thres)
f1_score(y_val, y_val_pred, average='macro')
0.7094767083062871

Le gain est modeste (+0,30%), mais reste dans les ordres de grandeur des optimisations de pipeline

modèle f1_macro_val
0 TfIdf_LR_opti_modif_seuil 0.709477
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432
0 TfIdf_LR_opti_ 0.699877
0 TfIdf_LR_prepro_opti_ 0.698565
0 base_TfIdf_RF_ 0.669789

4.2.3. Approches par transformers pré entraînés

Le traiteùent du langage est un sujet notoirement complexe. Les approches classiques utilisées précédement s’appuyaient sur des approches fréquentistes (Tf Idf / Bag Of Words) et le retraitement manuel de certains aspects (URL, utilisateurs cités etc.).

Une méthode qui a fait ses preuves ces dernières années est l’utilisation du Deep Learning de manière générale et de l’architecture BERT en particulier.

Dans un mode de fonctionnement optimal, on devrait reprndre BERT et réentrainer la dernière couche uniquement pour le sujet de classification étudié. Pour des raisons de temps et de compétence, ce n’est pas l’approche prise ici.

Dans ce rapport, nous avons repris un modèle pré-entrainé dérivé de BERT et mis à disposition par HuggingFace

HuggingFace

Plus précisement, le choix s’est porté sur le modèle roBERTa optimisé pour la tâche de classification de sentiment de Twitter

La difficulté principale rencontrée pour utiliser ce modèle a été d’adapter le fonctionnement du docker compose pour permettre l’accès aux ressources GPU du PC. Dans l’alternative, le temps de traitement était rédhibitoire.

4.2.3.1. Mise en place de l’environnement

import torch
torch.cuda.is_available()
True
from transformers import AutoModelForSequenceClassification
from transformers import TFAutoModelForSequenceClassification
from transformers import AutoTokenizer, AutoConfig
from transformers import pipeline


import numpy as np
from scipy.special import softmax
import csv
import urllib.request
# Preprocess text (username and link placeholders)
def preprocess(text):
    new_text = []


    for t in text.split(" "):
        t = '@user' if t.startswith('@') and len(t) > 1 else t
        t = 'http' if t.startswith('http') else t
        new_text.append(t)
    return " ".join(new_text)

Les modèles sont assez lourds (environ 500Mo)

Après avoir été téléchargé, il est important de réutiliser les documents sur disque

task='sentiment'
MODEL = f"cardiffnlp/twitter-roberta-base-{task}"

model = AutoModelForSequenceClassification.from_pretrained('/mnt/pretrained_models/'+MODEL)
tokenizer = AutoTokenizer.from_pretrained('/mnt/pretrained_models/'+MODEL)
config = AutoConfig.from_pretrained('/mnt/pretrained_models/'+MODEL)
# download label mapping
labels=[]
mapping_link = f"https://raw.githubusercontent.com/cardiffnlp/tweeteval/main/datasets/{task}/mapping.txt"
with urllib.request.urlopen(mapping_link) as f:
    html = f.read().decode('utf-8').split("\n")
    csvreader = csv.reader(html, delimiter='\t')
labels = [row[1] for row in csvreader if len(row) > 1]
nlp=pipeline("sentiment-analysis", model=model, tokenizer=tokenizer, device=0, return_all_scores=True)
def TorchTwitterRoBERTa_Pred(text = "Good night 😊"):
    text = preprocess(text)
    otpt = nlp(text)[0]
#    otpt = (list(otpt[i].values())[1] for i in range(len(otpt)))
    neg = otpt[0]['score']
    neu = otpt[1]['score']
    pos = otpt[2]['score']
    
#    NewName = {0:'roBERTa-neg', 1:'roBERTa-neu', 2:'roBERTa-pos'}
#    otpt = pd.json_normalize(otpt).transpose().rename(columns=NewName).reset_index().drop([0]).drop(columns=['index'])
    return neg, neu, pos
test = TorchTwitterRoBERTa_Pred()
test
(0.007609867490828037, 0.1458120346069336, 0.8465781211853027)

La partie précédente permettait de transcrire le code de Huggingface.

Néanmoins l’utilisation pour faire des prédictions sur l’intégralité d’une base peut vite être longue. Le code suivant permet d’optimiser le temps de parcours des données.

def run_loopy_roBERTa(df):
    v_neg, v_neu, v_pos = [], [], []
    for _, row in df.iterrows():
        v1, v2, v3 = TorchTwitterRoBERTa_Pred(row.values[0])
        v_neg.append(v1)
        v_neu.append(v2)
        v_pos.append(v3)
    df_result = pd.DataFrame({'roBERTa_neg': v_neg,
                              'roBERTa_neu': v_neu,
                              'roBERTa_pos': v_pos})
    df_result.set_index(df.index)
    return df_result

Afin d’utiliser la logique des pipelines, on crée une classe spécifique :

class clTwitterroBERTa(BaseEstimator, TransformerMixin):
    
    def __init__(self, field):
        self.field = field
        
    def fit(self, X, y=None):
        return self
    
    def transform(self, X):
        res = run_loopy_roBERTa(X[[self.field]])
        return res

4.2.3.2. roBERTa Twitter Sentiment

On dispose désormais de tous les éléments nécessaires. roBERTa ayant été entraîné sur 58M de tweets en anglais, nous n’avons pas à appliquer de preprocessing en dehors de la standardisation des adresses et utilisateurs prévus par défaut dans le code de Huggingface.

roBERTa_RF

roBERTa_pipe=Pipeline([
                     ('roBERTa', clTwitterroBERTa(field='text'))
                    ])
roBERTa_RF_Pipe = Pipeline(
    steps=[
        ('roBERTa', roBERTa_pipe),
        ("classifier", RandomForestClassifier(n_jobs=-1))
    ]
)
pipe = roBERTa_RF_Pipe


roBERTa_RF_= trainPipelineMlFlow(
                    mlf_XP = "Rapport",
                    xp_name_iter = "roBERTa_RF", 
                    pipeline = pipe, 
                    X_train = X_train, y_train = y_train, X_test = X_val, y_test = y_val,
                    target_col = 'sentiment',
                    fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42})
                    );
XP : roBERTa_RF 

pipeline : 
 Pipeline(steps=[('roBERTa',
                 Pipeline(steps=[('roBERTa', clTwitterroBERTa(field='text'))])),
                ('classifier',
                 RandomForestClassifier(n_jobs=-1, random_state=42))]) 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42} 

scores : 
 subset           train    test
metric                        
f1_macro           1.0  0.7059
precision_macro    1.0  0.7049
recall_macro       1.0  0.7070 

Test confusion matrix: 

elapsed time : 0:05:54.986998
../_images/test 4-mod_136_1.png
modèle f1_macro_val
0 TfIdf_LR_opti_modif_seuil 0.709477
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432
0 roBERTa_RF_ 0.705912
0 TfIdf_LR_opti_ 0.699877
0 TfIdf_LR_prepro_opti_ 0.698565
0 base_TfIdf_RF_ 0.669789

Sans optimisation, le modèle utilisant roBERTa tweet ne se place qu’en 4ème position, ce qui est en deçà des attentes a priori.

Par ailleurs, la quantité de mémoire vive à disposition sur la carte étant limitée, il n’a pas été possible d’effectuer une optimisation directe du pipeline, celle-ci créant des dépassements de mémoire.

C’est pourquoi la phase de prédiction par roBERTa tweet a été isolée (celle-ci ne présentant par ailleurs aps de possibilité de paramétrage) afin de laisser le seul classifier dans l’optimisation.

4.2.3.3. roBERTa Twitter Sentiment optimisé

X_train_roBERTa = roBERTa_pipe.transform(X_train)
X_val_roBERTa = roBERTa_pipe.transform(X_val)
X_test_roBERTa = roBERTa_pipe.transform(X_test)
X_train_roBERTa = X_train_roBERTa.set_index(X_train.index)
X_val_roBERTa = X_val_roBERTa.set_index(X_val.index)
X_test_roBERTa = X_test_roBERTa.set_index(X_test.index)
X_train_roBERTa
roBERTa_neg roBERTa_neu roBERTa_pos
0 0.064939 0.808318 0.126744
1 0.918158 0.066100 0.015742
2 0.924613 0.070741 0.004646
3 0.783082 0.192980 0.023938
4 0.564197 0.404574 0.031229
... ... ... ...
21979 0.798430 0.183766 0.017804
21980 0.108279 0.705047 0.186674
21981 0.913698 0.076074 0.010228
21982 0.006624 0.053324 0.940052
21983 0.150456 0.322249 0.527295

21984 rows × 3 columns

X_train_roBERTa.to_parquet('/mnt/data/interim/X_train_roBERTa.gzip',compression='gzip')
X_val_roBERTa.to_parquet('/mnt/data/interim/X_val_roBERTa.gzip',compression='gzip')
X_test_roBERTa.to_parquet('/mnt/data/interim/X_test_roBERTa.gzip',compression='gzip')
X_train_roBERTa = pd.read_parquet('/mnt/data/interim/X_train_roBERTa.gzip')
X_val_roBERTa = pd.read_parquet('/mnt/data/interim/X_val_roBERTa.gzip')
X_test_roBERTa = pd.read_parquet('/mnt/data/interim/X_test_roBERTa.gzip')

roBERTa_prepro

roBERTa_RF = Pipeline(
    steps=[
        ("classifier", RandomForestClassifier(n_jobs=-1))
    ]
)
pipe = roBERTa_RF

params = target_params(pipe, {
    "bootstrap": [True, False],
    "class_weight": ["balanced", None],
    "n_estimators": [100, 300, 500, 800, 1200],
    "max_depth": [5, 8, 15, 25, 30],
    "min_samples_split": [2, 5, 10, 15, 100],
    "min_samples_leaf": [1, 2, 5, 10]
})


roBERTa_RF_opti_ = trainPipelineMlFlow(
                    mlf_XP = "Rapport",
                    xp_name_iter="roBERTa_RF_opti", 
                    pipeline = pipe, 
                    X_train = X_train_roBERTa, y_train = y_train, X_test = X_val_roBERTa, y_test = y_val,
                    target_col = 'sentiment',
                    fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42}),
                    use_opti = True,
                    iterable_params=params,
                    n_iter=30
                    );
XP : roBERTa_RF_opti 

pipeline : 
 RandomizedSearchCV(cv=5,
                   estimator=Pipeline(steps=[('classifier',
                                              RandomForestClassifier(n_jobs=-1,
                                                                     random_state=42))]),
                   n_iter=30, n_jobs=-1,
                   param_distributions={'classifier__bootstrap': [True, False],
                                        'classifier__class_weight': ['balanced',
                                                                     None],
                                        'classifier__max_depth': [5, 8, 15, 25,
                                                                  30],
                                        'classifier__min_samples_leaf': [1, 2,
                                                                         5,
                                                                         10],
                                        'classifier__min_samples_split': [2, 5,
                                                                          10,
                                                                          15,
                                                                          100],
                                        'classifier__n_estimators': [100, 300,
                                                                     500, 800,
                                                                     1200]},
                   random_state=42, scoring='f1_macro') 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42, 'classifier__n_estimators': 1200, 'classifier__min_samples_split': 100, 'classifier__min_samples_leaf': 5, 'classifier__max_depth': 8, 'classifier__class_weight': None, 'classifier__bootstrap': True} 

scores : 
 subset            train    test
metric                         
f1_macro         0.7599  0.7466
precision_macro  0.7624  0.7488
recall_macro     0.7580  0.7450 

Test confusion matrix: 

elapsed time : 0:01:41.923590
../_images/test 4-mod_149_1.png

Le modèle, une fois optimisé, arrive en haut du classement avec un gain de persque +4% de f1 pour atteindre 74,66% sur le jeu de validation

modèle f1_macro_val
0 roBERTa_RF_opti_ 0.746630
0 TfIdf_LR_opti_modif_seuil 0.709477
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432
0 roBERTa_RF_ 0.705912
0 TfIdf_LR_opti_ 0.699877
0 TfIdf_LR_prepro_opti_ 0.698565
0 base_TfIdf_RF_ 0.669789

4.2.3.4. Essai combinaison de différentes méthodes

Afin de gagner encore en performance, il est possible de combiner plusieurs outils d’estimation de sentimenst a priori. Ces transformations ne relevant pas des mêmes stratégies, elles capturent des éléments légèrement différents.

Les méthodes sélectionnées ici pour leur simplicité d’utilisation sont :

roBERTa_Blob_Vader

class Blob(BaseEstimator, TransformerMixin):
    def __init__(self, field):
        self.field = field
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        X[['polarity', 'subjectivity']] =  X[self.field].apply(lambda x:TextBlob(x).sentiment).apply(pd.Series)
        return X[['polarity', 'subjectivity']]
blob_pipe=Pipeline([
                     ('blob', Blob(field='text'))
                    ])
X_train_Blob=blob_pipe.transform(X_train)
X_val_Blob=blob_pipe.transform(X_val)
X_test_Blob=blob_pipe.transform(X_test)
X_train_Blob.head()
polarity subjectivity
0 0.000000 0.0
1 -0.976562 1.0
2 0.000000 0.0
3 0.000000 0.0
4 0.000000 0.0
X_train_Blob.to_parquet('/mnt/data/interim/X_train_Blob.gzip',compression='gzip')
X_val_Blob.to_parquet('/mnt/data/interim/X_val_Blob.gzip',compression='gzip')
X_test_Blob.to_parquet('/mnt/data/interim/X_test_Blob.gzip',compression='gzip')
X_train_Blob = pd.read_parquet('/mnt/data/interim/X_train_Blob.gzip')
X_val_Blob = pd.read_parquet('/mnt/data/interim/X_val_Blob.gzip')
X_test_Blob = pd.read_parquet('/mnt/data/interim/X_test_Blob.gzip')

On vérifie que TextBlob et roBERTa ne capturent pas les mêmes éléments.

TextBlob fournissant un indicateur global, on approxime les sentiments de rBERTa comme positive - negative

X =pd.DataFrame(columns=['roBERTa_sent'])
X['roBERTa_sent'] = X_train_roBERTa['roBERTa_pos']- X_train_roBERTa['roBERTa_neg']
X2 = pd.concat([X, X_train_Blob[['polarity']]], axis=1)
X2.corr()
roBERTa_sent polarity
roBERTa_sent 1.000000 0.563574
polarity 0.563574 1.000000
fig = px.scatter(x = X_train_roBERTa['roBERTa_pos']- X_train_roBERTa['roBERTa_neg'], 
                 y = X_train_Blob['polarity'],
                labels = {
                     'x': 'roBERTa',
                     'y' : 'TextBLob - polarity',
                 },
                title = 'Comparaison des sentiments roBERTa vs TextBlob')
fig.show()
class Vader(BaseEstimator, TransformerMixin):
    def __init__(self, field):
        self.field = field
        sid = SentimentIntensityAnalyzer()
    def fit(self, X, y=None):
        return self
    def transform(self, X):
        sid = SentimentIntensityAnalyzer()
        X[['neg', 'neu', 'pos', 'compound']] =  X[self.field].apply(sid.polarity_scores).apply(pd.Series)
        return X[['neg', 'neu', 'pos', 'compound']]
vader_pipe=Pipeline([
                     ('vader', Vader(field='text'))
                    ])
X_train_Vader=vader_pipe.transform(X_train)
X_val_Vader=vader_pipe.transform(X_val)
X_test_Vader=vader_pipe.transform(X_test)
X_train_Vader.head()
neg neu pos compound
0 0.000 1.000 0.0 0.0000
1 0.474 0.526 0.0 -0.7437
2 0.494 0.506 0.0 -0.5994
3 0.538 0.462 0.0 -0.3595
4 0.000 1.000 0.0 0.0000
X_train_Vader.to_parquet('/mnt/data/interim/X_train_Vader.gzip',compression='gzip')
X_val_Vader.to_parquet('/mnt/data/interim/X_val_Vader.gzip',compression='gzip')
X_test_Vader.to_parquet('/mnt/data/interim/X_test_Vader.gzip',compression='gzip')
X_train_Vader = pd.read_parquet('/mnt/data/interim/X_train_Vader.gzip')
X_val_Vader = pd.read_parquet('/mnt/data/interim/X_val_Vader.gzip')
X_test_Vader = pd.read_parquet('/mnt/data/interim/X_test_Vader.gzip')

On vérifie de la même manière que Vader et roBERTa ne capturent pas les mêmes éléments.

Pour les positifs

X_pos = pd.concat([X_train_roBERTa[['roBERTa_pos']], X_train_Vader[['pos']]], axis=1)
X_pos.corr()
roBERTa_pos pos
roBERTa_pos 1.000000 0.607805
pos 0.607805 1.000000
fig = px.scatter(x = X_train_roBERTa['roBERTa_pos'], 
                 y = X_train_Vader['pos'],
                labels = {
                     'x': 'roBERTa',
                     'y' : 'Vader',
                 },
                title = 'Comparaison des sentiments roBERTa vs Vaders - positive')
fig.show()

Pour les neutres

X_pos = pd.concat([X_train_roBERTa[['roBERTa_neu']], X_train_Vader[['neu']]], axis=1)
X_pos.corr()
roBERTa_neu neu
roBERTa_neu 1.000000 0.473356
neu 0.473356 1.000000
fig = px.scatter(x = X_train_roBERTa['roBERTa_neu'], 
                 y = X_train_Vader['neu'],
                labels = {
                     'x': 'roBERTa',
                     'y' : 'Vader',
                 },
                title = 'Comparaison des sentiments roBERTa vs Vaders - neutral')
fig.show()

Pour les négatifs

X_pos = pd.concat([X_train_roBERTa[['roBERTa_neg']], X_train_Vader[['neg']]], axis=1)
X_pos.corr()
roBERTa_neg neg
roBERTa_neg 1.000000 0.560029
neg 0.560029 1.000000
fig = px.scatter(x = X_train_roBERTa['roBERTa_neg'], 
                 y = X_train_Vader['neg'],
                labels = {
                     'x': 'roBERTa',
                     'y' : 'Vader',
                 },
                title = 'Comparaison des sentiments roBERTa vs Vaders - negative')
fig.show()

On peut alors calculer la base agrégée

X_train_compound = pd.concat([X_train_roBERTa, X_train_Blob, X_train_Vader], axis=1)
X_val_compound = pd.concat([X_val_roBERTa, X_val_Blob, X_val_Vader], axis=1)
X_test_compound = pd.concat([X_test_roBERTa, X_test_Blob, X_test_Vader], axis=1)
X_train_compound.head()
roBERTa_neg roBERTa_neu roBERTa_pos polarity subjectivity neg neu pos compound
0 0.064939 0.808318 0.126744 0.000000 0.0 0.000 1.000 0.0 0.0000
1 0.918158 0.066100 0.015742 -0.976562 1.0 0.474 0.526 0.0 -0.7437
2 0.924613 0.070741 0.004646 0.000000 0.0 0.494 0.506 0.0 -0.5994
3 0.783082 0.192980 0.023938 0.000000 0.0 0.538 0.462 0.0 -0.3595
4 0.564197 0.404574 0.031229 0.000000 0.0 0.000 1.000 0.0 0.0000
X_val_compound.head()
roBERTa_neg roBERTa_neu roBERTa_pos polarity subjectivity neg neu pos compound
21984 0.562985 0.367200 0.069815 0.0 0.0000 0.211 0.789 0.000 -0.1531
21985 0.226580 0.667382 0.106038 0.0 0.0000 0.000 1.000 0.000 0.0000
21986 0.002332 0.476234 0.521434 0.0 0.0000 0.000 1.000 0.000 0.0000
21987 0.296688 0.569384 0.133927 0.0 0.0000 0.000 0.799 0.201 0.7003
21988 0.004010 0.177016 0.818974 0.4 0.5125 0.000 0.864 0.136 0.4588

Tip

Comme on travaille avec des arbres il n’y a pas besoin de renormer / standardiser les différentes colonnes

pipe = roBERTa_RF

params = target_params(pipe, {
    "bootstrap": [True, False],
    "class_weight": ["balanced", None],
    "n_estimators": [100, 300, 500, 800, 1200],
    "max_depth": [5, 8, 15, 25, 30],
    "min_samples_split": [2, 5, 10, 15, 100],
    "min_samples_leaf": [1, 2, 5, 10]
})


roBERTa_Blob_Vader_RF_opti_ = trainPipelineMlFlow(
                                    mlf_XP="Rapport",
                                    xp_name_iter="roBERTa_Blob_Vader_RF_opti", 
                                    pipeline = pipe, 
                                    X_train = X_train_compound, y_train = y_train, X_test = X_val_compound, y_test = y_val,
                                    target_col = 'sentiment',
                                    fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42}),
                                    use_opti = True,
                                    iterable_params = params,
                                    n_iter = 30
                                    );
XP : roBERTa_Blob_Vader_RF_opti 

pipeline : 
 RandomizedSearchCV(cv=5,
                   estimator=Pipeline(steps=[('classifier',
                                              RandomForestClassifier(n_jobs=-1,
                                                                     random_state=42))]),
                   n_iter=30, n_jobs=-1,
                   param_distributions={'classifier__bootstrap': [True, False],
                                        'classifier__class_weight': ['balanced',
                                                                     None],
                                        'classifier__max_depth': [5, 8, 15, 25,
                                                                  30],
                                        'classifier__min_samples_leaf': [1, 2,
                                                                         5,
                                                                         10],
                                        'classifier__min_samples_split': [2, 5,
                                                                          10,
                                                                          15,
                                                                          100],
                                        'classifier__n_estimators': [100, 300,
                                                                     500, 800,
                                                                     1200]},
                   random_state=42, scoring='f1_macro') 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42, 'classifier__n_estimators': 500, 'classifier__min_samples_split': 15, 'classifier__min_samples_leaf': 10, 'classifier__max_depth': 15, 'classifier__class_weight': None, 'classifier__bootstrap': True} 

scores : 
 subset            train    test
metric                         
f1_macro         0.8301  0.7567
precision_macro  0.8333  0.7591
recall_macro     0.8276  0.7546 

Test confusion matrix: 

elapsed time : 0:02:30.393318
../_images/test 4-mod_185_1.png
modèle f1_macro_val
0 roBERTa_Blob_Vader_RF_opti_ 0.756699
0 roBERTa_RF_opti_ 0.746630
0 TfIdf_LR_opti_modif_seuil 0.709477
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432
0 roBERTa_RF_ 0.705912
0 TfIdf_LR_opti_ 0.699877
0 TfIdf_LR_prepro_opti_ 0.698565
0 base_TfIdf_RF_ 0.669789

Note

L’utilisation de Vader et Blob en soutien de roBERTa a permi de gagner 1 point de f1 sur le jeu de validation

4.2.4. Essai xgboost sur combinaison de méthodes

Dans ce dernier essai on remplace le RandomClassifier par un XGBoost

xgb

import xgboost as xgb
roBERTa_xgb = Pipeline(
    steps=[
        ("classifier", xgb.XGBClassifier())
    ]
)

Tip

Dans l’exemple ci-dessous on utilise explicitement le GPU pour accélérer les calculs (tree_method = 'gpu_hist' et gpu_id=0) . Le gain est saisissant : dans une première version qui n’utilisait que le CPU, le modèle tournait en 3h30, contre un peu plus de 4 min ici, soit un gain de temps de presque 1:52!

pipe = roBERTa_xgb

params = target_params(pipe, {
     "eta"    : [0.05, 0.10, 0.15, 0.20, 0.25, 0.30 ] ,
     "max_depth"        : [ 3, 4, 5, 6, 8, 10, 12, 15],
     "min_child_weight" : [ 1, 3, 5, 7 ],
     "gamma"            : [ 0.0, 0.1, 0.2 , 0.3, 0.4 ],
     "colsample_bytree" : [ 0.3, 0.4, 0.5 , 0.7 ]
     })


roBERTa_xgb_opti_ = trainPipelineMlFlow(
                    mlf_XP="DSA_Tweets",
                    xp_name_iter="roBERTa - xgb - opti", 
                    pipeline = pipe, 
                    X_train = X_train_compound, y_train = y_train, X_test = X_val_compound, y_test = y_val,
                    target_col = 'sentiment',
                    fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42, 'gpu_id':0, 'tree_method' : 'gpu_hist'}),
                    use_opti = True,
                    iterable_params=params,
                    n_iter=20
                    )
[03:37:26] WARNING: ../src/learner.cc:1061: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'multi:softprob' was changed from 'merror' to 'mlogloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
XP : roBERTa - xgb - opti 

pipeline : 
 RandomizedSearchCV(cv=5,
                   estimator=Pipeline(steps=[('classifier',
                                              XGBClassifier(base_score=None,
                                                            booster=None,
                                                            colsample_bylevel=None,
                                                            colsample_bynode=None,
                                                            colsample_bytree=None,
                                                            gamma=None,
                                                            gpu_id=0,
                                                            importance_type='gain',
                                                            interaction_constraints=None,
                                                            learning_rate=None,
                                                            max_delta_step=None,
                                                            max_depth=None,
                                                            min_child_weight=None,
                                                            missing=nan,
                                                            monotone_constra...
                                                            scale_pos_weight=None,
                                                            subsample=None,
                                                            tree_method='gpu_hist',
                                                            validate_parameters=None,
                                                            verbosity=None))]),
                   n_iter=20, n_jobs=-1,
                   param_distributions={'classifier__colsample_bytree': [0.3,
                                                                         0.4,
                                                                         0.5,
                                                                         0.7],
                                        'classifier__gamma': [0.0, 0.1, 0.2,
                                                              0.3, 0.4],
                                        'classifier__max_depth': [3, 4, 5, 6, 8,
                                                                  10, 12, 15],
                                        'classifier__min_child_weight': [1, 3,
                                                                         5,
                                                                         7]},
                   random_state=42, scoring='f1_macro') 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42, 'classifier__gpu_id': 0, 'classifier__tree_method': 'gpu_hist', 'classifier__min_child_weight': 7, 'classifier__max_depth': 5, 'classifier__gamma': 0.1, 'classifier__colsample_bytree': 0.7} 

scores : 
 subset            train    test
metric                         
f1_macro         0.8196  0.7591
precision_macro  0.8223  0.7614
recall_macro     0.8174  0.7572 

Test confusion matrix: 

elapsed time : 0:04:54.512383
../_images/test 4-mod_194_1.png

On essaye de comprendre les 27 tweets négatifs qui ont été prédits positifs

Le modèle XGBoost optimisé à partir des données augmenté se hisse à la première place du podium

modèle f1_macro_val
0 roBERTa_xgb_opti_ 0.759147
0 roBERTa_Blob_Vader_RF_opti_ 0.756699
0 roBERTa_RF_opti_ 0.746630
0 TfIdf_LR_opti_modif_seuil 0.709477
0 base_TfIdf_RF_prepro_ 0.707919
0 base_TfIdf_RF_prepro_opti_ 0.706432
0 roBERTa_RF_ 0.705912
0 TfIdf_LR_opti_ 0.699877
0 TfIdf_LR_prepro_opti_ 0.698565
0 base_TfIdf_RF_ 0.669789

On peut s’interroger sur les prédiction restantes qui sont positives, mais classées comme négatives et inversement

y_val_pred = roBERTa_xgb_opti_.predict(X_val_compound)
inpt = pd.concat([X_val, X_val_compound], axis=1)
exemples_realNeg_predPos_fin = inpt[(y_val['sentiment']==-1) & (y_val_pred==1)]
exemples_realPos_predNeg_fin = inpt[(y_val['sentiment']==1) & (y_val_pred==-1)]
exemples_realNeg_predPos_fin
text roBERTa_neg roBERTa_neu roBERTa_pos polarity subjectivity neg neu pos compound
22095 Omg I want TF2, everybody on my Steam Friends list is playing it 0.001319 0.039033 0.959648 0.000000 0.000000 0.000 0.592 0.408 0.6369
22222 Hi, my name is Kate and I`m addicted to mm`s! 0.009058 0.103601 0.887340 -0.500000 0.600000 0.000 1.000 0.000 0.0000
22424 I miss my dog r.i.p.Batman... Yeah, Batman (I really hope `all dogs go to heaven` is true) 0.073255 0.355204 0.571541 0.275000 0.425000 0.080 0.650 0.270 0.5849
22498 My storm is acting up ....Excited for the discussion session regarding Social Media. Scott Lake, CEO of ThinkSM, will be attending. 0.000800 0.013065 0.986135 0.016667 0.033333 0.000 1.000 0.000 0.0000
23387 it`s ridiculously warm in bed 0.003796 0.075537 0.920667 0.600000 0.600000 0.329 0.411 0.260 -0.1280
23874 i want to see my bud mel miss hur loads 0.084246 0.431542 0.484211 0.000000 0.000000 0.162 0.707 0.131 -0.0772
23932 and this is better 4 me than a snickers bar or hersheys w/almonds.... which i use to b addicted to b4. i miss them LOL 0.029847 0.111354 0.858799 0.300000 0.600000 0.064 0.679 0.257 0.7034
24100 AHHHHHHH!!!!! its my 16th birthday And i cant belive i found out im seeing yous tonight Best present everr!!! <333333333 0.001637 0.006869 0.991494 0.500000 0.150000 0.000 0.760 0.240 0.7482
24443 Hav fun at heav y Metal happy hour you guys! In the future accadentally sets it on fire while smoking with 0.002921 0.033595 0.963485 0.433333 0.441667 0.090 0.637 0.273 0.7088
24468 So glad the days almost over... Another nite of me nd my pain pills alone at the crib lol. Ughh I wish this weekend was over alreadi! 0.038555 0.112199 0.849246 0.750000 0.850000 0.150 0.593 0.257 0.5838
24789 where`s enthusiasm in meeeee 0.332726 0.539401 0.127872 0.000000 0.000000 0.000 0.508 0.492 0.4404
24850 Good work.I`ve only just managed to turn my studio on... I envy your productivity 0.008150 0.049628 0.942222 0.350000 0.800000 0.131 0.688 0.181 0.2023
25027 I`m feeling much less alone now in my love for Fitzcarraldo, most people I mention it to have no idea what I am talking about. 0.060577 0.296912 0.642512 0.277778 0.388889 0.137 0.664 0.199 0.4201
25245 () Gonna watch JT on SNL tonight - not a fan of his music but think he`s hilarious! `**** in my Pants` - WAY too funny 0.043971 0.154075 0.801954 0.437500 1.000000 0.049 0.634 0.317 0.8413
25911 Ate too much vegetarian pizza for dinner! But it was so good 0.002520 0.014339 0.983142 0.475000 0.400000 0.000 0.671 0.329 0.7509
25923 - God i`m up early. Hayley still asleep but today is party day so i`m getting stuff ready. x 0.016226 0.263445 0.720329 0.150000 0.400000 0.000 0.626 0.374 0.8100
26321 TIRED! goodnight twitter its mother`s day happy mother`s day lov my moomy <3 yayy! God Bless. 0.002120 0.015923 0.981957 0.433333 0.900000 0.136 0.412 0.452 0.8152
26328 Oh great Tampa people - anybody in the area know someone who works for the Jain Society of Tampa Bay? None of their phone #`s work. 0.290720 0.496840 0.212440 0.800000 0.750000 0.000 0.854 0.146 0.6249
26464 Steve makes fruit smoothies for me each day & they are berry delicious, I made mine today & it was berry, berry bad 0.011197 0.097974 0.890829 0.150000 0.833333 0.139 0.714 0.147 0.0516
26529 They have a list of 50 state parks here in PA that are under consideration for closing. Nice ones too. 0.018955 0.241769 0.739276 0.600000 1.000000 0.000 0.865 0.135 0.4215
26561 I would totally take you to prom...if i hadnt already gone sorry lol 0.057130 0.328177 0.614693 0.100000 0.816667 0.000 0.691 0.309 0.4628
26576 its too sunny for work !!! 0.108557 0.347588 0.543854 0.000000 0.000000 0.000 0.576 0.424 0.5684
26740 The 22nd can`t get here fast enough! 0.008742 0.081710 0.909548 0.100000 0.550000 0.000 1.000 0.000 0.0000
26900 Happy Mother`s Day to every mommy out there 0.001472 0.015549 0.982979 0.800000 1.000000 0.000 0.654 0.346 0.5719
27060 Bruno arghhhh i cant wait 0.004612 0.025054 0.970334 0.000000 0.000000 0.000 1.000 0.000 0.0000
27164 Obviously not too bad 0.074839 0.330566 0.594595 -0.350000 0.583333 0.000 0.513 0.487 0.4310
27219 I can`t believe you tweeted that. It was our special moment 0.104980 0.375221 0.519799 0.357143 0.571429 0.000 0.769 0.231 0.4019

Contrairement à la première analyse, on observe que le taux de tweets vraisemblablement mal libellés semble plus important

exemples_realPos_predNeg_fin
text roBERTa_neg roBERTa_neu roBERTa_pos polarity subjectivity neg neu pos compound
22162 I wish I can see that. They have CNN here again, with no volume. 0.634638 0.326405 0.038957 0.000000 0.000000 0.148 0.671 0.181 0.1280
22281 _22 ok so I`m having a complete insomniac moment. It`s 6am(almost) and I`m STILL awake. I hate when I can`t stop thinking! mornin! 0.929382 0.059761 0.010857 -0.133333 0.600000 0.252 0.662 0.086 -0.6467
22404 i hope my morning show doesn`t get cancelled! 0.548575 0.370015 0.081410 0.000000 0.000000 0.196 0.491 0.313 0.2942
22630 let me guess ... ran a few miles? Respect dude, I can`t do it. Maybe you should train me 0.712335 0.261885 0.025779 -0.200000 0.100000 0.000 0.838 0.162 0.4767
22807 Why don`t we close the library due to the great weather? And the ac isn`t working #fb 0.701326 0.255291 0.043382 0.337500 0.562500 0.000 0.796 0.204 0.6249
22817 Why do you hurt me? Does it bring you joy to see me cry? You know I love you more then anything and yet u break my heart everyday! 0.929708 0.057478 0.012813 0.387500 0.475000 0.170 0.603 0.227 0.4857
23044 Big Laptop is too big, so it`s time to switch to the Eee. Bye big guy 0.590308 0.326236 0.083455 0.000000 0.100000 0.000 1.000 0.000 0.0000
23281 Aww just read your tweet. I`m not sure about later either (work too) feel it for us 0.577661 0.382728 0.039611 0.016667 0.596296 0.109 0.891 0.000 -0.2411
23308 My palms are itchy. Doesn`t that mean something about coming into a great deal of money? 0.598176 0.352266 0.049558 0.243750 0.718750 0.109 0.677 0.214 0.4588
23402 Good morning I don`t think it has stopped raining once for the past three days or so, but who cares? 0.287069 0.541327 0.171604 0.225000 0.425000 0.061 0.672 0.267 0.7092
23536 I gave up cable in these tough economic times. it was either cable or shoes, and you know what cable lost 0.776729 0.203081 0.020190 -0.094444 0.516667 0.174 0.826 0.000 -0.4215
23610 _mcc bye, Scotty! i`m gonna miss you. ily<333 0.093740 0.366023 0.540237 0.000000 0.000000 0.213 0.787 0.000 -0.2244
23784 Funtime was not a lot of fun!! But finally done with it 0.835564 0.122180 0.042256 0.234375 0.600000 0.196 0.804 0.000 -0.3474
23864 on that note - i do not feel missed. 0.601743 0.335804 0.062453 0.000000 0.000000 0.000 0.761 0.239 0.2235
24016 morning world, is raining 2day so revision don`t seem so tough, 0.039120 0.401251 0.559628 -0.388889 0.833333 0.166 0.834 0.000 -0.2479
24305 ._.; Thanxxx ! Now with that message I just wanna leave !! )= ! BYE ! 0.682855 0.271553 0.045592 0.000000 0.000000 0.177 0.823 0.000 -0.3331
24435 IIII know!!! and mean 0.598222 0.349651 0.052128 -0.312500 0.687500 0.000 1.000 0.000 0.0000
24623 whooaaa. just got an overwheolming itus attack after eating 0.833472 0.154779 0.011749 0.000000 0.000000 0.279 0.721 0.000 -0.4767
24628 Is dreading going to work BUT....its friiiiiday!! whoop!!! 0.593392 0.300574 0.106034 0.000000 0.000000 0.395 0.605 0.000 -0.6776
24740 dude... Can you really be a bachelor at this point?? Don`t worry about it. 0.456769 0.477203 0.066028 0.200000 0.200000 0.214 0.786 0.000 -0.5040
24757 No. That would be too easy. All I have is the user manual which is not enough for me to claim his bike 0.609687 0.338870 0.051443 0.216667 0.666667 0.088 0.797 0.116 0.1779
24848 i think i`ll be home more than i want to be next week - no work booked in for the forseeable. 0.040266 0.400941 0.558794 0.250000 0.250000 0.111 0.809 0.080 -0.1585
24928 two macaroons go into a bar....one says oh your a nut. wow I need to get out more. 0.675843 0.256036 0.068120 0.300000 0.750000 0.000 0.787 0.213 0.5859
25016 While on vacation, having golden times spamming Google by 5.350 redirects. http://bit.ly/18kwzh 0.308829 0.552101 0.139071 0.300000 0.500000 0.220 0.780 0.000 -0.4767
25061 i want so bad to go to the mcfly`s concert anybody up to go with me? 0.310271 0.435570 0.254159 -0.700000 0.666667 0.210 0.719 0.072 -0.5413
25195 they are all over one is a fan with a vip and the other one is the winner of the twisted vid het si weer eens raar gelopen, chaos 0.392492 0.533716 0.073792 -0.312500 0.687500 0.102 0.637 0.260 0.6908
25335 thinking about new.. oh yes .. btw bankroll stays at $14.88.. so down a bit from yesterday.. and I won`t whine about bad beats .. 0.046912 0.496715 0.456373 -0.239731 0.470034 0.209 0.697 0.094 -0.5106
25340 Oh no! I hope you find your kitten 0.616610 0.311418 0.071972 0.000000 0.000000 0.212 0.481 0.307 0.2481
25398 There was no traffic at all on my way home and all traffic lights were green.Im afraidIowe karma a big check 0.331551 0.444313 0.224136 0.000000 0.100000 0.104 0.896 0.000 -0.2960
25788 _Jo my mask is non-existent at the mo Charis didn`t send me one & I haven`t been bothered to make one! I`m wearing boy clothes! 0.653728 0.305131 0.041140 0.000000 0.000000 0.116 0.884 0.000 -0.4374
25807 http://twitpic.com/4hbs5 - ahahahahahahahaha can i please eat that off your head **** 0.804986 0.171911 0.023103 0.000000 0.000000 0.000 0.796 0.204 0.3182
26008 i really wish i could make it! a 12 hr. drive just isn`t going to happen this weekend. 0.633561 0.278867 0.087571 0.250000 0.200000 0.000 0.810 0.190 0.5081
26191 Full, thanks for the food Jean I should have brought that half of the watermelon with me and eat it on the freeway and crash and die. 0.362044 0.414240 0.223716 0.127778 0.305556 0.202 0.673 0.125 -0.5423
26230 awwww this made me realize I have to take down my bulletin board too! There`s so many memories up there. 0.585399 0.332485 0.082117 0.268519 0.562963 0.000 1.000 0.000 0.0000
26628 _eyes I wish I knew! The curse of Tumblr. 0.696642 0.250489 0.052870 0.000000 0.000000 0.330 0.435 0.235 -0.2714
26662 ty for feeding our NK addiction..ermm i mean our uhh nope yup addiction covers it 0.399613 0.436190 0.164197 -0.312500 0.687500 0.000 0.833 0.167 0.3818
26665 I`m so bunged up!! I Hate colds!! 0.963905 0.029016 0.007078 -1.000000 0.900000 0.506 0.494 0.000 -0.7296
26673 I`ve burnt my collerbone and arms and face! aww! 0.941750 0.050507 0.007743 0.375000 0.900000 0.000 1.000 0.000 0.0000
26720 I know, right?!? I have such a lead foot 0.651567 0.300968 0.047465 0.178571 0.517857 0.000 1.000 0.000 0.0000
26780 **left off the 'again' in the title...whoops! 0.606299 0.344978 0.048722 0.000000 0.000000 0.000 1.000 0.000 0.0000
26787 i`m gonna miss all the live Comet action tomorrow! i have to go take care of my cousins and they don`t have access to the interwebz 0.834206 0.148361 0.017433 0.130682 0.300000 0.057 0.819 0.124 0.4389
26872 Agreed! Though Eclipse apps hinder collecting the heap dump by catching OOME. Had to muck about in JConsole 0.748078 0.222225 0.029696 0.600000 0.900000 0.138 0.762 0.100 -0.2003
26879 _Khan oh please, you don`t have to do that to me. Don`t bother 0.764604 0.222715 0.012681 0.000000 0.000000 0.153 0.701 0.146 -0.0258
27042 Urghh, I`m gonna do my project now don`t wanna waste valuable weekend time 0.603666 0.331370 0.064964 -0.200000 0.000000 0.166 0.651 0.183 0.0772
27063 I was looking forward to seeing in Raleigh (fan for 10 years NB too) but scalpers took the tix and sell them for $200 morons 0.713850 0.240869 0.045281 -0.800000 1.000000 0.114 0.886 0.000 -0.4497
27180 Oh no I hope you reach him! 0.466357 0.410455 0.123188 0.000000 0.000000 0.232 0.316 0.452 0.2714
27190 friday night is my fav night of the week but now I have to go to stupid dog training classes 0.837420 0.134528 0.028051 -0.800000 1.000000 0.195 0.720 0.085 -0.5574
27210 Ooooh! I thought eggos were some kind of synthetic egg or egg sustitute or something. You crazy Americans 0.903786 0.086572 0.009642 0.000000 0.900000 0.144 0.856 0.000 -0.4003
27370 i have a crush on someone! 0.231675 0.588623 0.179702 0.000000 0.000000 0.387 0.613 0.000 -0.2244
27427 i hate my presentation hahah whatever im glad its over 0.705794 0.207846 0.086359 -0.150000 0.950000 0.270 0.511 0.219 -0.1779
27457 I really wish someone would make a groupchat theme for Adium suited for IRC. yMous has way too low contrast. 0.686272 0.264261 0.049467 0.100000 0.250000 0.100 0.759 0.142 0.2247

On peut essayer de comprendre comment le modèle exploite les informations fournies pour prendre ses décisions en s’appuyant sur SHAP

#set the tree explainer as the model of the pipeline
explainer = shap.TreeExplainer(roBERTa_xgb_opti_.best_estimator_['classifier'])

#apply the preprocessing to x_test
#observations = pipeline['imputer'].transform(x_test)
observations = X_val_compound

#get Shap values from preprocessed data
shap_values = explainer.shap_values(observations)

#plot the feature importance
titres = {0 : 'Prédictions négatives', 1: 'Prédictions neutres', 2 : 'Prédictions positives'}
for i in range(3):
    shap.summary_plot(shap_values[i], observations, plot_type="bar", show=False)
    plt.title(titres[i])
    plt.show()
    
../_images/test 4-mod_208_0.png ../_images/test 4-mod_208_1.png ../_images/test 4-mod_208_2.png

Tip

Cette première analyse confirme que le modèle s’appuie principalement sur les prédiction de roBERTa tweet de la classification idoine pour prendre sa décision. Les autres composantes ayant des contributions bien plus faibles.

On peut ensuite zoomer sur la manière dont les prédictions de roBERTa sont prises en compte dans le modèle :

titres = {0 : 'Prédictions négatives', 1: 'Prédictions neutres', 2 : 'Prédictions positives'}
axe = {0 : 'roBERTa_neg', 1: 'roBERTa_neu', 2 : 'roBERTa_pos'}
for i in range(3):
    shap.dependence_plot(axe[i], shap_values[i], X_val_compound, show=False)
    plt.title(titres[i])
    plt.show()
../_images/test 4-mod_211_0.png ../_images/test 4-mod_211_1.png ../_images/test 4-mod_211_2.png

4.3. Soumission finale

On réentraine les 2 modèles finalistes sur le jeu de validation sur l’intégralité de tain + val et on évalue sur le jeu de test

Warning

Ici on réajuste uniquement les modèles sans explorer de nouvelles vvaleur d’hyperparamètres

y_train_tot = pd.concat([y_train, y_val], axis=0)
X_train_compound_tot = pd.concat([X_train_compound, X_val_compound], axis=0)
X_train_compound_tot
roBERTa_neg roBERTa_neu roBERTa_pos polarity subjectivity neg neu pos compound
0 0.064939 0.808318 0.126744 0.000000 0.000000 0.000 1.000 0.000 0.0000
1 0.918158 0.066100 0.015742 -0.976562 1.000000 0.474 0.526 0.000 -0.7437
2 0.924613 0.070741 0.004646 0.000000 0.000000 0.494 0.506 0.000 -0.5994
3 0.783082 0.192980 0.023938 0.000000 0.000000 0.538 0.462 0.000 -0.3595
4 0.564197 0.404574 0.031229 0.000000 0.000000 0.000 1.000 0.000 0.0000
... ... ... ... ... ... ... ... ... ...
27475 0.434403 0.445122 0.120474 0.000000 0.000000 0.128 0.722 0.150 0.1027
27476 0.139542 0.635024 0.225433 0.184091 0.646970 0.000 0.890 0.110 0.3818
27477 0.003337 0.022629 0.974034 0.366667 0.533333 0.000 0.572 0.428 0.9136
27478 0.053331 0.357756 0.588913 0.300000 0.100000 0.000 0.680 0.320 0.3291
27479 0.012305 0.150569 0.837125 0.000000 0.000000 0.000 0.458 0.542 0.8074

27480 rows × 9 columns

pipe = roBERTa_RF

params = target_params(pipe, {
    'n_jobs':-1,
    'random_state':42,
    'n_estimators': 500, 
    'classifier__min_samples_split': 15, 
    'classifier__min_samples_leaf': 10, 
    'classifier__max_depth': 15, 
    'classifier__class_weight': None, 
    'classifier__bootstrap': True
})


roBERTa_Blob_Vader_RF_opti_tot_ = trainPipelineMlFlow(
                                        mlf_XP="Rapport",
                                        xp_name_iter="roBERTa_Blob_Vader_RF_opti_tot", 
                                        pipeline = pipe, 
                                        X_train = X_train_compound_tot, y_train = y_train_tot, X_test = X_test_compound, y_test = y_test,
                                        target_col = 'sentiment',
                                        fixed_params = params,
                                        use_opti = False,
                                        );
XP : roBERTa_Blob_Vader_RF_opti_tot 

pipeline : 
 Pipeline(steps=[('classifier',
                 RandomForestClassifier(n_estimators=500, n_jobs=-1,
                                        random_state=42))]) 

params: 
 {'classifier__n_jobs': -1, 'classifier__random_state': 42, 'classifier__n_estimators': 500} 

scores : 
 subset           train    test
metric                        
f1_macro           1.0  0.7502
precision_macro    1.0  0.7504
recall_macro       1.0  0.7503 

Test confusion matrix: 

elapsed time : 0:00:03.972592
../_images/test 4-mod_218_1.png
res_fin=item
pipe = roBERTa_xgb

params = target_params(pipe, {
                'classifier__n_jobs': -1, 
                'classifier__random_state': 42, 
                'classifier__gpu_id': 0, 
                'classifier__tree_method': 'gpu_hist', 
                'classifier__min_child_weight': 7, 
                'classifier__max_depth': 5, 
                'classifier__gamma': 0.1, 
                'classifier__colsample_bytree': 0.7
     })


roBERTa_xgb_opti_tot_ = trainPipelineMlFlow(
                    mlf_XP="Rapport",
                    xp_name_iter="roBERTa_xgb_opti_tot", 
                    pipeline = pipe, 
                    X_train = X_train_compound_tot, y_train = y_train_tot, X_test = X_test_compound, y_test = y_test,
                    target_col = 'sentiment',
                    fixed_params = params,
                    use_opti = False
                    );
[10:22:08] WARNING: ../src/learner.cc:1061: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'multi:softprob' was changed from 'merror' to 'mlogloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
XP : roBERTa_xgb_opti_tot 

pipeline : 
 Pipeline(steps=[('classifier',
                 XGBClassifier(base_score=0.5, booster='gbtree',
                               colsample_bylevel=1, colsample_bynode=1,
                               colsample_bytree=1, gamma=0, gpu_id=0,
                               importance_type='gain',
                               interaction_constraints='',
                               learning_rate=0.300000012, max_delta_step=0,
                               max_depth=6, min_child_weight=1, missing=nan,
                               monotone_constraints='()', n_estimators=100,
                               n_jobs=-1, num_parallel_tree=1,
                               objective='multi:softprob', random_state=42,
                               reg_alpha=0, reg_lambda=1, scale_pos_weight=None,
                               subsample=1, tree_method='gpu_hist',
                               validate_parameters=1, verbosity=None))]) 

params: 
 {} 

scores : 
 subset            train    test
metric                         
f1_macro         0.8582  0.7600
precision_macro  0.8600  0.7603
recall_macro     0.8567  0.7598 

Test confusion matrix: 

elapsed time : 0:00:01.275500
../_images/test 4-mod_221_1.png
res_fin=res_fin.append(item)
modèle f1_macro_val f1_macro_test
0 roBERTa_xgb_opti_ 0.759147 0.759953
1 roBERTa_Blob_Vader_RF_opti_ 0.756699 0.750216
2 roBERTa_RF_opti_ 0.746630 NaN
3 TfIdf_LR_opti_modif_seuil 0.709477 NaN
4 base_TfIdf_RF_prepro_ 0.707919 NaN
5 base_TfIdf_RF_prepro_opti_ 0.706432 NaN
6 roBERTa_RF_ 0.705912 NaN
7 TfIdf_LR_opti_ 0.699877 NaN
8 TfIdf_LR_prepro_opti_ 0.698565 NaN
9 base_TfIdf_RF_ 0.669789 NaN

Note

Le modèle final présente un f1 macro de 0.76 sur le jeu de test.

Les résultats sont en ligne avec ceux obtenus sur le jeu de validation. Néanmoins le fait de ne pas avoir regarder les résultats sur le jeu de test avant cette étape assure que nous n’avons pas pu faire de leakage d’une manière ou d’une autre. Cette démarche émule aussi celle qui existerait dans un cas industriel où une équipe séparée serait en charge de définir un jeu de test permettant de tester le modèle dans des conditions dégradées par rapport à son domaine d’entraînement pour en assurer la stabilité en production.